package com.google.code.p.narcissus.core;

import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.LinkedBlockingQueue;

import com.google.code.p.narcissus.core.model.ComparisonResult;
import com.google.code.p.narcissus.core.model.ReferenceMetaData;
import com.google.code.p.narcissus.core.model.ReferencePicture;
import com.google.code.p.narcissus.core.model.ComparisonResult.PictureComparisonResult;

/**
 * <p>
 * Credit to all the complex algorithms go to Ivo KRAB.
 * </p>
 * 
 * @author Ivo KRAB
 * @author Jérôme JADOULLE [JADOJ] - 30 juil. 2010
 */
public class PictureComparator {

	public ComparisonResult compare(ReferencePicture reference, BufferedImage screenshot) throws NarcissusException {
		return compare(reference, screenshot, 10);
	}

	public ComparisonResult compare(BufferedImage reference, BufferedImage screenshot) throws NarcissusException {
		return compare(new ReferencePicture(reference, null), screenshot, 10);
	}

	/**
	 * <p>
	 * Pass <code>null</code> for the <code>referenceMetaData</code> parameter if there are no exclusions
	 * </p>
	 */
	public ComparisonResult compare(BufferedImage reference, ReferenceMetaData referenceMetaData, BufferedImage screenshot) throws NarcissusException {
		return compare(new ReferencePicture(reference, referenceMetaData), screenshot, 10);
	}

	/**
	 * <p>
	 * The <code>thresold</code> parameter tells the quantity of pixels around pixels in error to check to decide whether the set of pixels in error is part of the same error
	 * rectangle or not. A thresold of 2 means that, if a pixel in error is found at a maximum of two pixels left, right, top or bottom of another pixel in error, then these will be
	 * said to be part of the same error rectangle.
	 * </p>
	 * 
	 * @throws NarcissusException
	 *           if the quantity of pixels in the reference and the screenshot is not the same (we won't bother comparing them in this case).
	 */
	private ComparisonResult compare(ReferencePicture reference, BufferedImage screenshot, int threshold) throws NarcissusException {
		int referencePixelQuantity = reference.getImage().getWidth() * reference.getImage().getHeight();

		if (referencePixelQuantity != screenshot.getWidth() * screenshot.getHeight()) {
			return new ComparisonResult(null, null, threshold, PictureComparisonResult.verificationFail);
		}

		List<Rectangle> errorRectangles = new ArrayList<Rectangle>();
		List<Rectangle> falsePositiveRectangles = new ArrayList<Rectangle>();
		boolean[] checkedPixels = new boolean[referencePixelQuantity];
		if (reference.getReferenceMetaData() != null) {
			unflagFalsePositivePixels(checkedPixels, reference.getImage().getWidth(), reference.getReferenceMetaData());
		}

		int x = 0, y = 0;

		while (y < reference.getImage().getHeight()) {
			while (x < reference.getImage().getWidth()) {
				if (!isPixelAlreadyChecked(x, y, reference.getImage().getWidth(), checkedPixels)) {
					setPixelAsChecked(x, y, reference.getImage().getWidth(), checkedPixels);
					if (reference.getImage().getRGB(x, y) != screenshot.getRGB(x, y)) {
						Rectangle errorRectangle = createErrorRectangle(x, y, threshold, checkedPixels, reference.getImage().getWidth(), reference.getImage().getHeight(),
						    reference.getImage(), screenshot);
						if (!isFalsePositive(errorRectangle)) {
							errorRectangles.add(errorRectangle);
						} else {
							falsePositiveRectangles.add(errorRectangle);
						}
					}
				}
				x++;
			}
			y++;
			x = 0;
		}

		return new ComparisonResult(errorRectangles, falsePositiveRectangles, threshold);
	}

	/**
	 * Checks whether the rectangle passed in parameter is a false positive or not.
	 * <p>
	 * A false positive is a zone that, even if different from the original, is not to be saved as-is.<br />
	 * Blinking caret zones are a very good example of such zones.
	 * </p>
	 * <p>
	 * Implementation of this method is currently intentionnally very limited. It is expected to be improved as new false-positive zones appear...
	 * </p>
	 */
	private boolean isFalsePositive(Rectangle errorRectangle) {
		return Integer.valueOf(1).equals(((Double) errorRectangle.getWidth()).intValue()) && Integer.valueOf(16).equals(((Double) errorRectangle.getHeight()).intValue());
	}

	/**
	 * Extracted from calling method to allow easy testing
	 */
	void unflagFalsePositivePixels(boolean[] checkedPixels, int referencePictureWidth, ReferenceMetaData referenceExclusions) {
		for (Rectangle currentExclusionRectangle : referenceExclusions.getExclusionZones()) {
			for (int heightCounter = 0; heightCounter < currentExclusionRectangle.getHeight(); heightCounter++) {
				for (int widthCounter = 0; widthCounter < currentExclusionRectangle.getWidth(); widthCounter++) {
					checkedPixels[((Double) currentExclusionRectangle.getX()).intValue() + widthCounter + referencePictureWidth
					    * (heightCounter + ((Double) currentExclusionRectangle.getY()).intValue())] = true;
				}
			}
		}
	}

	private Rectangle createErrorRectangle(int x, int y, int thresold, boolean[] checkedPixels, int width, int height, BufferedImage reference, BufferedImage screenshot) {
		Rectangle errorRectangle = new Rectangle(x, y, 1, 1);
		computeErrorRectangle(x, y, thresold, checkedPixels, width, height, reference, screenshot, errorRectangle);

		return errorRectangle;
	}

	/**
	 * Gets the error rectangle encompassing the pixel in error (set by the <code>x</code> and <code>y</code> parameters).
	 * <p>
	 * This method is written to be called recursively.<br />
	 * When the oldest call to this method exits, the error rectangle returned is final and absolute. No other error found in the image can have an impact in this one.
	 * </p>
	 */
	private void computeErrorRectangle(int x, int y, int threshold, boolean[] checkedPixels, int width, int height, BufferedImage reference, BufferedImage screenshot,
	    Rectangle errorRectangle) {

		LinkedBlockingQueue<ComparisonZone> zones = new LinkedBlockingQueue<ComparisonZone>();
		ComparisonZone currentZone = new ComparisonZone(x, y, threshold, width, height);
		while (currentZone != null) {
			for (int currentY = currentZone.getMinY(); currentY <= currentZone.getMaxY(); currentY++) {
				for (int currentX = currentZone.getMinX(); currentX <= currentZone.getMaxX(); currentX++) {
					if (!isPixelAlreadyChecked(currentX, currentY, reference.getWidth(), checkedPixels)) {
						setPixelAsChecked(currentX, currentY, reference.getWidth(), checkedPixels);
						if (reference.getRGB(currentX, currentY) != screenshot.getRGB(currentX, currentY)) {
							// redefine error rectangle size
							if (currentX < errorRectangle.x) {
								errorRectangle.width += (errorRectangle.x - currentX);
								errorRectangle.x = currentX;
							} else if (currentX >= errorRectangle.x + errorRectangle.width) {
								errorRectangle.width += (currentX - (errorRectangle.x + errorRectangle.width - 1));
							}

							if (currentY < errorRectangle.y) {
								errorRectangle.height += (errorRectangle.y - currentY);
								errorRectangle.y = currentY;
							} else if (currentY >= errorRectangle.y + errorRectangle.height) {
								errorRectangle.height += (currentY - (errorRectangle.y + errorRectangle.height - 1));
							}

							zones.add(new ComparisonZone(currentX, currentY, threshold, width, height));
						}
					}
				}
			}
			currentZone = zones.poll();
		}
	}

	boolean isPixelAlreadyChecked(int x, int y, int pictureWidth, boolean[] checkedPixels) {
		return checkedPixels[y * pictureWidth + x];
	}

	void setPixelAsChecked(int x, int y, int pictureWidth, boolean[] checkedPixels) {
		checkedPixels[y * pictureWidth + x] = true;
	}
}

class ComparisonZone {
	private int minX, maxX, minY, maxY;

	public int getMinX() {
		return minX;
	}

	public int getMaxX() {
		return maxX;
	}

	public int getMinY() {
		return minY;
	}

	public int getMaxY() {
		return maxY;
	}

	public ComparisonZone(int x, int y, int threshold, int width, int height) {
		minX = x - threshold;
		if (minX < 0) {
			minX = 0;
		}

		maxX = x + threshold;
		if (maxX >= width) {
			maxX = width - 1;
		}

		minY = y - threshold;
		if (minY < 0) {
			minY = 0;
		}

		maxY = y + threshold;
		if (maxY >= height) {
			maxY = height - 1;
		}
	}
}
